/*
 * Copyright (C) 2011-2012 Adam Gregoire <bsdunx@gmail.com>
 * Copyright (C) 2011-2012 Jeff Hubbard <lord2800@gmail.com>
 *
 * This file is part of the ScriptKit scripting engine library.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include "config.hpp"

#if defined _DEBUG && defined OS_WIN32 && defined WANT_CRT_DEBUG
#include "CrtDebug.hpp"

#undef THIS_FILE
static char THIS_FILE[] = __FILE__;

#define new DEBUG_NEW
#define malloc(x) MALLOC_DBG(x)

#endif

#ifdef OS_WIN32
#include <Windows.h>
#endif

#ifdef HAVE_STL_THREAD
#include <thread>
#else
#include <boost/thread.hpp>
#endif

#include <iostream>
#include <string>
#include <algorithm>
#include <fstream>
#include <ctime>
#include <cassert>

#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef HAVE_DLFCN_H
#include <dlfcn.h>
#endif

#include "Exports.hpp"

#include "jsapi.h"
#include "jsapi_ext.hpp"
#include "jsapi_ext_util.hpp"

#include "SMKit.hpp"
#include "ScriptKit.hpp"

namespace ScriptKit
{
	JSAPI_EMPTY_CTOR_BODY(Global)

	JSClass global = { "Global", JSCLASS_GLOBAL_FLAGS, JSCLASS_DEFAULT_WITH_CTOR(Global) };

	JSFunctionSpec module_methods[] =
	{
		JS_FS("load",		mod_load,		1,	JSPROP_STATIC),
		JS_FS("unload",		mod_unload,		1,	JSPROP_STATIC),
		JS_FS("require",	mod_require,	1,	JSPROP_STATIC),
		{ nullptr }
	};

	JSAPI_FUNC(mod_load)
	{
		auto engine = (SMKit*)JS_GetRuntimePrivate(JS_GetRuntime(cx));

		JS_SET_RVAL(cx, vp, JSVAL_VOID);

		JSString* farg = nullptr;
		if(!JS_ConvertArguments(cx, argc, JS_ARGV(cx, vp), "S", &farg))
			return JS_FALSE;

		const size_t len = JS_GetStringEncodingLength(cx, farg);
		char* cstr = new char[len+1];
		memset(cstr, 0, len+1);
		size_t convcount = JS_EncodeStringToBuffer(farg, cstr, len);
		if(convcount == (size_t)-1 || convcount > len)
		{
			// FIXME
			return JS_FALSE;
		}

		bool found = false;
		std::string path;

		auto paths = engine->GetSearchPaths();
		for(auto it(paths.crbegin()), end(paths.crend()); it != end; ++it)
		{
			if(*it == "")
				path = cstr;
			else
				path = *it + "/" + cstr;

			found = engine->FileExists(path);
			if(found)
				break;
		}
		
		if(!found)
		{
			// TODO: wrap the resultant Script in an object and return it to the caller
			auto script = engine->CompileScript(path);
			if(script)
			{
				//JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(script));
				engine->RunScript(script);
			}
		}

		JS_free(cx, cstr);

		if(!found)
			return JS_ThrowError(cx, "Script not found");

		return JS_TRUE;
	}

	JSAPI_FUNC(mod_unload)
	{
		return JS_TRUE;
	}

	JSAPI_FUNC(mod_require)
	{
		auto engine = static_cast<SMKit*>(JS_GetRuntimePrivate(JS_GetRuntime(cx)));
		auto script = static_cast<SMKit::Script*>(JS_GetContextPrivate(cx));

		JS_SET_RVAL(cx, vp, JSVAL_VOID);

		JSString* farg = nullptr;
		if(!JS_ConvertArguments(cx, argc, JS_ARGV(cx, vp), "S", &farg))
			return JS_FALSE;

		const size_t len = JS_GetStringEncodingLength(cx, farg);
		char* cstr = new char[len+1];
		memset(cstr, 0, len+1);
		size_t convcount = JS_EncodeStringToBuffer(farg, cstr, len);
		if(convcount == (size_t)-1 || convcount > len)
		{
			// FIXME
			return JS_FALSE;
		}

		SMKit::Script::Module* module = nullptr;
		std::string filepath;

		auto paths = engine->GetSearchPaths();
		for(auto it(paths.crbegin()), end(paths.crend()); it != end; ++it)
		{
			if(*it == "")
				filepath = cstr;
			else
				filepath = *it + "/" + cstr;
	
			// TODO: internal modules do not have an extension, so we can shortcut if 
			// no extension is provided.

			module = engine->GetModule(script, filepath);
			if(module)
			{
				JS_free(cx, cstr);
				JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(module->exports));
				return JS_TRUE;
			}
			else
			{
				if(engine->FileExists(filepath))
				{
					module = engine->CompileModule(script, filepath);
					if(module)
					{
						JS_free(cx, cstr);
						JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(module->exports));
						return JS_TRUE;
					}
				}
			}
		}

		JS_free(cx, cstr);

		return JS_ThrowError(cx, "Module not found");
	}

	JSAPI_FUNC(mod_addpath)
	{

		return JS_TRUE;
	}

	// Implementation of our interface contract
	bool Initialize()
	{
#ifdef OS_WIN32
# if defined _DEBUG && defined _CRTDBG_MAP_ALLOC
		_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF|_CRTDBG_CHECK_ALWAYS_DF);
		_CrtSetReportHook(&CrtDebugReportHook);
# endif
		HMODULE hModNSPR = LoadLibraryExW(L"libnspr4.dll", nullptr, 0);
		HMODULE hModMozJS = LoadLibraryExW(L"mozjs.dll", nullptr, 0);
#else
		void* hModNSPR = dlopen("libnspr4.so", RTLD_LAZY);
		void* hModMozJS = dlopen("libmozjs.so", RTLD_LAZY);
#endif
		return true;
	}

	SMKit::SMKit(std::string& path)
		/*JSClassInitCallback classCallback = nullptr,*/
		//JSErrorReporter reporter) :
	/* , *classInit(classCallback), reporter(reporter)*/
	{
#ifdef OS_POSIX
		if(path == "")
			rootPath = "/";
#else
		if(path == "")
			rootPath = "";
#endif
		paths.push_back("");
	}

	SMKit::~SMKit(void)
	{
		for(auto begin(scripts.cbegin()), end(scripts.cend()); begin != end; ++begin)
			delete *begin;

		JS_ShutDown();
	}

	std::vector<SMKit::Script*> SMKit::ScriptList(void) const
	{
		return scripts;
	}

	std::string& SMKit::NormalizePath(std::string &path)
	{
		std::replace(path.begin(), path.end(), '\\', '/');

#ifdef OS_WIN32
		std::transform(path.begin(), path.end(), path.begin(), tolower);
#endif

		return path;
	}

	bool SMKit::FileExists(std::string& file)
	{
#ifdef OS_WIN32
		struct __stat64 stat;
		return _stat64(file.c_str(), &stat) != 0 ? false : true;
#else
		struct stat64 stat;
		return stat64(file.c_str(), &stat) != 0 ? false : true;
#endif
	}

	// FIXME use streambuf?
	bool SMKit::LoadFile(std::string & file, char **fbuf)
	{
		std::fstream stream;

		stream.open(file, std::ios::in|std::ios::binary|std::ios::ate);
		if(!stream.is_open())
			return false;

		if(!stream.good())
			return false;

		std::streamoff len = stream.tellg();
		*fbuf = new char[(std::streamoff)(len+1)];
		stream.seekg(0, std::ios::beg);
		stream.read(*fbuf, len);

		if(!stream.good())
		{
			delete [] fbuf;
			return false;
		}

		(*fbuf)[len] = '\0';

		stream.close();
		return true;
	}

	IScript* SMKit::GetScript(std::string& fileName)
	{
		for(auto begin(scripts.cbegin()), end(scripts.cend()); begin != end; ++begin)
		{
			if((*begin)->fileName == fileName)
				return reinterpret_cast<IScript*>(*begin);
		}
		return nullptr;
	}

	bool SMKit::Shutdown()
	{
		delete this;
		return true;
	}

	const std::vector<std::string>& SMKit::GetSearchPaths(void) const
	{
		return paths;
	}

	SMKit::Script* SMKit::CreateScriptObject(uint32_t runtimeSize, uint32_t stackChunkSize)
	{
		JSRuntime* rt = JS_NewRuntime(runtimeSize);
		if(!rt)
			return nullptr;
		
		JS_SetRuntimePrivate(rt, this);

		JS_SetContextCallback(rt, &SMKit::ContextCallback);

		JSContext* cx = JS_NewContext(rt, stackChunkSize);
		if(!cx)
		{
			JS_DestroyRuntime(rt);
			return nullptr;
		}

		JSAutoRequest req(cx);
		JSObject* global = JS_NewCompartmentAndGlobalObject(cx, &ScriptKit::global, nullptr);

		JSAutoEnterCompartment comp;
		if(!comp.enter(cx, global))
			return nullptr;

		if(JS_InitStandardClasses(cx, global) == JS_FALSE)
			return nullptr;

		if(JS_DefineFunctions(cx, global, module_methods) == JS_FALSE)
			return nullptr;

		Script* script = new Script(rt, cx, global, runtimeSize, stackChunkSize);
	
		JS_SetContextPrivate(cx, script);

		if(!InitModules(script))
			return nullptr;

		return script;
	}

	// FIXME clean this up
	IScript* SMKit::CompileScript(std::string& fileName)
	{
		if(!rootPath.empty())
		{
			if(rootPath.back() == '/')
				rootPath + fileName;
			else
				rootPath + "/" + fileName;
		}
		NormalizePath(fileName);

#if OS_WIN32
		struct _stat st;
		if(_stat(fileName.c_str(), &st) != 0)
		{
			std::cout << "Unable to stat file" << std::endl;
			return nullptr;
		}
#else
		struct stat st;
		if(stat(fileName.c_str(), &st) != 0)
		{
			return nullptr;
		}
#endif
		std::string fileDate(ctime(&st.st_mtime));

		// Compute hashes for the files name and date strings
		size_t fileNameHash = std::hash<std::string>()(fileName);
		size_t fileDateHash = std::hash<std::string>()(fileDate);

		Script* script = reinterpret_cast<Script*>(GetScript(fileName));
		if(script != nullptr)
		{
			// Named script was found in the script list, see if it needs to be 
			// recompiled.
			//
			// Unless the user has disabled file modification updates in their 
			// file-system this should be enough to detect if a script has changed.
			// If no change is detected just return the script object we looked up.

			// FIXME need to figure out how object will be replaced
			if(script->fileDateHash == fileDateHash)
				return reinterpret_cast<IScript*>(script);
		}

		// Script was not in the list or date hash does not match so we load
		// the file and compile it.

		// FIXME kill running script
		//if(script != nullptr)
		//	;;

		Script* newScript = CreateScriptObject(16L*1024L*1024L, 8192L);
		if(newScript == nullptr)
			return nullptr;

		newScript->fileName = fileName;
		newScript->fileDate = fileDate;
		newScript->fileNameHash = fileNameHash;
		newScript->fileDateHash = fileDateHash;

		{
			JSAutoRequest req(newScript->cx);
			JSAutoEnterCompartment comp;
			if(!comp.enter(newScript->cx, newScript->global))
				return nullptr;

			// FIXME for now I'm just keeping it simple
			// JS_CompileUCScript(cx, obj, (const jschar*)buf, strlen(buf), __FILE__, __LINE__);
			newScript->jsscript = JS_CompileUTF8File(newScript->cx, newScript->global, fileName.c_str());
			if(!newScript->jsscript)
				return nullptr;
		}

		JS_ClearRuntimeThread(newScript->rt);

		scripts.push_back(newScript);
		return reinterpret_cast<IScript*>(newScript);
	}

	JSObject* SMKit::GetConstructor(Script* script, JSClass* classp)
	{
		return script->constructors[classp];
	}

	void SMKit::RegisterConstructor(Script* script, JSClass* classp, JSObject* ctor)
	{
		script->constructors[classp] = ctor;
	}

	void SMKit::DefineClasses(Script* script, JSObject* exports, JSClassSpec* classes)
	{
		for(JSClassSpec* spec = classes; spec->classp != nullptr; spec++)
		{
			JSObject* proto = nullptr;
			if(spec->proto != nullptr)
				proto = GetConstructor(script, spec->proto);

			JSObject* classp = JS_InitClass(script->cx, exports, 
					proto, spec->classp, spec->ctor, spec->argc,
					spec->properties, spec->methods, 
					spec->static_properties, spec->static_methods);
			if(classp)
			{
				RegisterConstructor(script, spec->classp, classp);
			}
		}
	}

	JSModuleSpec* SMKit::GetModuleDefinition(JSModuleSpec* modspec)
	{
		for(auto it = mod_specs.cbegin(), end = mod_specs.cend(); it != end; ++it)
		{
			if((*it)->name == modspec->name)
				return *it;
		}

		return nullptr;
	}

	bool SMKit::RegisterModuleDefinition(IScript::Module::moduledef_t* definition)
	{
		JSModuleSpec* mod_spec = reinterpret_cast<JSModuleSpec*>(definition);
		if(GetModuleDefinition(mod_spec) == nullptr)
			mod_specs.push_back(mod_spec);

		return true;
	}

	SMKit::Script::Module* SMKit::CompileModule(Script* script, std::string& file)
	{
		NormalizePath(file);

		Script::Module* module = GetModule(script, file);
		if(module)
			return module;

		std::string fullpath(rootPath + "/" + file);

		
		return module;
	}

	SMKit::Script::Module* SMKit::InitModule(Script* script, std::string& file)
	{
		Script::Module* module = new Script::Module;

		{
			JSAutoRequest req(script->cx);
			JSAutoEnterCompartment comp;
			comp.enter(script->cx, script->global);

			module->global = JS_NewGlobalObject(script->cx, &global);
			JS_AddNamedObjectRoot(script->cx, &module->global, "module global");
			

			module->exports = JS_NewObject(script->cx, nullptr, nullptr, nullptr);
			JS_AddObjectRoot(script->cx, &script->global);

			JS_DefineProperty(script->cx, script->global, "exports", OBJECT_TO_JSVAL(module->exports), nullptr, nullptr, JSPROP_DEFAULT);
		}

		return nullptr;
	}

	SMKit::Script::Module* SMKit::InitModule(Script* script, JSObject* obj, JSModuleSpec* modSpec)
	{
		Script::Module* module = new Script::Module;

		module->global = JS_NewGlobalObject(script->cx, &ScriptKit::global);
		if(!module->global)
			return nullptr;

		if(!JS_InitStandardClasses(script->cx, module->global))
			return nullptr;

		module->exports = JS_NewObject(script->cx, nullptr, nullptr, module->global);
		if(!module->exports)
			return nullptr;

		JS_AddNamedObjectRoot(script->cx, &module->global, "module global");

		JS_DefineProperty(script->cx, module->global, "exports", OBJECT_TO_JSVAL(module->exports), nullptr, nullptr, JSPROP_DEFAULT);
		
		if(modSpec->classes != nullptr)
		{
			DefineClasses(script, module->exports, modSpec->classes);
			if(modSpec->classes->static_methods != nullptr)
				if(JS_DefineFunctions(script->cx, module->exports, modSpec->classes->static_methods) == JS_FALSE)
					return nullptr;
			if(modSpec->classes->static_properties != nullptr)
				if(JS_DefineProperties(script->cx, module->exports, modSpec->classes->static_properties) == JS_FALSE)
					return nullptr;
		}

		module->name = modSpec->name;
		module->context = script->cx;

		script->modules.push_back(module);

		return module;
	}

	bool SMKit::InitModules(Script* script)
	{
		JSAutoRequest req(script->cx);
		JSAutoEnterCompartment comp;
		if(!comp.enter(script->cx, script->global))
			return false;

		bool success = false;
		// Loop though all registered module specs.
		for(auto it = mod_specs.begin(), end = mod_specs.end(); it != end; ++it)
		{
			success = InitModule(script, script->global, *it) == nullptr ? false : true;
		}

		return success;
	}

	SMKit::Script::Module* SMKit::GetModule(Script* script, std::string& name)
	{
		for(auto it = script->modules.begin(), end = script->modules.end(); it != end; ++it)
		{
			if((*it)->name == name)
				return *it;
		}
		return nullptr;
	}

	// FIXME Improve this once engine is in a reasonable condition
	bool SMKit::RunScript(IScript* script)
	{
		if(script)
		{
			auto smScript = reinterpret_cast<Script*>(script);
#ifdef HAVE_STL_THREAD
			smScript->thr = std::move(std::thread(&SMKit::Worker, this, smScript));
#else
			smScript->thr = boost::thread(&SMKit::Worker, this, smScript);
#endif
			return true;
		}

		return false;
	}

	bool SMKit::Worker(Script* script)
	{
		assert(script != nullptr);
		if(!script)
			return false;

		bool success = false;

		JS_SetRuntimeThread(script->rt);
		{
			JSAutoRequest req(script->cx);
			JSAutoEnterCompartment comp;
			if(!comp.enter(script->cx, script->global))
			{
				return false;
			}

			jsval rval = JSVAL_NULL;
			success = JS_ExecuteScript(script->cx, script->global, script->jsscript, &rval) == JS_TRUE;

		}
		JS_ClearRuntimeThread(script->rt);

		return success;
	}

	JSBool SMKit::ContextCallback(JSContext *cx, unsigned contextOp)
	{
		if(contextOp == JSCONTEXT_NEW)
		{
			// set options, register handlers
#ifdef _DEBUG
			JS_SetGCZeal(cx, 2, 1);
#endif
			JS_SetOperationCallback(cx, &OperationCallback);
			JS_SetErrorReporter(cx, &ErrorReporter);
			JS_SetVersion(cx, JSVERSION_LATEST);
			JS_SetOptions(cx, JSOPTION_METHODJIT| 
					JSOPTION_RELIMIT|JSOPTION_VAROBJFIX|JSOPTION_STRICT);
		}

		return JS_TRUE;
	}

	JSBool SMKit::OperationCallback(JSContext *cx)
	{
		Script* self = (Script*)JS_GetContextPrivate(cx);
		JSRuntime* rt = JS_GetRuntime(cx);

		while(self->state == State::PAUSED)
		{
			JSAutoSuspendRequest req(cx);
			JS_GC(rt);
		}
#if 0
		switch(self->state)
		{
		case Paused: {
			JSAutoSuspendRequest req(cx);
			WaitForSingleObject(self->pause, INFINITE);
			break;
					 }
		case Stopping: {
			// remove any handlers, set the state, etc.
			self->RemoveListeners();

			JS_ReportError(cx, "Aborted");
			JS_ReportPendingException(cx);

			break;
					   }
		default: /* do nothing */ break;
		}

		ScriptEvent* entry = (ScriptEvent*)InterlockedPopEntrySList(&(self->events));
		if(entry != nullptr) EventProc(entry);
#endif

		return JS_TRUE;
	}


	void SMKit::ErrorReporter(JSContext *cx, const char *message, JSErrorReport *report)
	{
		JSRuntime* rt = JS_GetRuntime(cx);
		if(rt)
		{		
			//Engine* engine = (Engine*)JS_GetRuntimePrivate(rt);
			//if(engine && engine->reporter)
			//	engine->reporter(cx, message, report);

			std::cout << "[";
			if(JSREPORT_IS_STRICT(report->flags))
				std::cout << "Strict ";
			if(JSREPORT_IS_WARNING(report->flags))
				std::cout << "Warning";
			else
				std::cout << "Error";
			std::cout << "] ";

			if(report->filename != nullptr)
				std::cout << "At " << report->filename << ":" << report->lineno;
			std::cout << " (" << report->errorNumber << ") " << message << std::endl;

		}
	}

}
